@epxing/cli 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # @epxing/cli
2
2
 
3
- 独立的产品/项目开发工具,包含创建、Vue3 迁移、静态检查和升级技能安装。CLI 使用自己的版本号,目标平台由 `--epx-frame` 指定;发布包没有 npm 运行时依赖,不包含 Vue、Element Plus、设计器或 `epx-frame`。迁移代码与框架仓库的 `tools/` 共用,打包时编译为独立 Node 程序。
3
+ 独立的工程开发工具,包含交互菜单、开发服务、工程与产品库构建、创建、Vue3 迁移、静态检查和升级技能安装。CLI 使用自己的版本号,目标平台由 `--epx-frame` 指定;发布包没有 npm 运行时依赖。启动、构建调用当前工程的脚本及本地依赖;迁移代码与框架仓库的 `tools/` 共用,打包时编译为独立 Node 程序。
4
4
 
5
5
  ```powershell
6
- npm install -g @epxing/cli@0.1.0 --registry https://registry.npmjs.org/
6
+ npm install -g @epxing/cli@0.2.0 --registry https://registry.npmjs.org/
7
+ epx # ↑↓ + Enter 选择;Esc / Ctrl+C 取消
8
+ epx dev --port 3000
9
+ epx build --mode prd
10
+ epx lib # 产品库整体构建;平台目录构建聚合组件库
11
+ epx lib --lazy # 产品按需库(工程需支持此构建形态)
7
12
  epx create my-product --epx-frame <目标平台版本>
8
13
  epx migrate E:/products/old-product --epx-frame <目标平台版本>
9
14
  epx migrate E:/products/old-product --epx-frame <目标平台版本> --apply
@@ -12,7 +17,33 @@ epx skill install --dir E:/products/my-product
12
17
 
13
18
  如已全局安装旧的 `epx-frame` CLI,先用 `npm uninstall -g epx-frame` 释放 `epx` 命令。产品工程里的 `epx-frame` 依赖保留。工具不在安装阶段运行迁移、下载平台或改产品文件;产品仍需安装目标框架依赖才能开发和构建。
14
19
 
15
- `create` 和 `migrate --apply` 自动写入 `runtime: "vue3"`。迁移默认预览;apply 要求目标 Git 工作树干净。实际执行 `.vue` 转换时才按需安装并缓存 gogocode。标识表示迁移目标,仍需完成转换、构建和业务复测。
20
+ 使用 fnm/nvm 时,不同 Node 版本的全局包分别安装。切换到日常使用的 Node 版本后更新 CLI,再用 `epx help` 确认开头为 `@epxing/cli`、`epx --version` 确认版本。旧仓库的 `pnpm epx` 仍是该仓库自己的脚本。
21
+
22
+ ## 命令
23
+
24
+ | 命令 | 行为 |
25
+ | --- | --- |
26
+ | `epx` / `epx menu` | 交互选择操作、工程目录、环境、端口和构建方式;管道支持数字输入 |
27
+ | `epx dev` / `epx serve` | 使用工程 `dev`、`serve` 或 `start` 脚本启动服务 |
28
+ | `epx build` | 使用工程的构建脚本;只有 `build:lib` 的产品自动构建产品库 |
29
+ | `epx lib` / `epx build --lib` | 优先产品 `build:lib` / `lib`,平台使用完整 `build:component`;识别旧 `--target lib` 构建脚本 |
30
+ | `epx lib --lazy` | 优先 `build:lib:lazy`,否则为产品库脚本设置 `LAZY_LIB=true`;工程需支持按需构建 |
31
+ | `epx preview` | 使用 `preview` / `vite:preview`;没有脚本时使用工程本地 Vite |
32
+ | `epx create` | 无参数进入创建引导,也可用参数直接创建 |
33
+ | `epx migrate` / `epx upgrade` | 无参数引导:目标版本、迁移范围、预览、确认应用 |
34
+ | `epx skill install` | 安装技能,支持自定义目录和明确覆盖 |
35
+ | `epx scan` / `epx verify` | 迁移风险扫描 / 本地与线上组件清单校验 |
36
+ | `epx lint [--fix]` / `epx circular` | 执行工程已有的代码检查、循环依赖检查脚本 |
37
+ | `epx remote` / `epx pkg <包名\|all>` | 使用平台已有 `build:remote` / `build:pkg` / `build:pkgs` 脚本 |
38
+ | `epx help` / `epx --version` | 帮助 / 工具版本 |
39
+
40
+ 工程命令支持 `--dir <目录>`,默认从当前目录向上找到最近的 `package.json`;不会改为 CLI 全局安装目录。没有项目脚本时,dev/build/preview 可使用项目已安装的 Vite,不会自动下载构建工具。工程仍需先安装自身依赖。
41
+
42
+ `--mode/-m`、`--port/-p` 支持旧的 `-mprd`、`-p3000` 写法。build 优先匹配 `build:<mode>`;`localize` 也匹配 `build:local`,其余情况向构建脚本传入 `--mode`。平台 `build:component` 包含多步构建,不追加 `--mode`,环境由工程脚本统一配置。
43
+
44
+ 平台 `epx build` 保留 `--thin`、`--no-designer`、`--no-dashboard`;产品工程不接受平台裁剪选项。需向工程脚本透传其他参数时使用 `--`,如 `epx dev -- --host 0.0.0.0`。脚本原有的 pre/post 生命周期、自定义环境和多步构建会保留,失败退出码向上传递。
45
+
46
+ `create` 和 `migrate --apply` 自动写入 `runtime: "vue3"`。带参数直接调用时 `--epx-frame` 必填,交互引导也会要求输入目标平台。迁移默认预览;apply 要求目标 Git 工作树干净。实际执行 `.vue` 转换时才按需安装并缓存 gogocode。标识表示迁移目标,仍需完成转换、构建和业务复测。
16
47
 
17
48
  `epx scan <目录>` 扫描静态风险,`epx verify <目录> --online <地址|文件>` 对照组件清单。技能安装到 `.claude/skills/epxing-frontend-vue3-migrate/`,可用 `--skills-dir` 指定其他客户端要求的位置。已有技能默认不覆盖;确认替换使用 `--force`。平台基类源码分析仍需准备对应版本源码或文档。
18
49
 
@@ -21,7 +52,7 @@ epx skill install --dir E:/products/my-product
21
52
  ```powershell
22
53
  cd frontend/frame/packages/cli
23
54
  npm login --registry https://registry.npmjs.org/
24
- # 首次使用 0.1.0;后续发布前调整为未发布过的新版本
55
+ # 发布前调整为未发布过的新版本
25
56
  # npm version <新的CLI版本> --no-git-tag-version
26
57
  $previousPublicPublish = $env:EPX_PUBLISH_PUBLIC
27
58
  try {
package/dist/cli.mjs CHANGED
@@ -4,6 +4,8 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { spawnSync } from 'node:child_process'
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url'
7
+ import { findProject, runProject } from './project.mjs'
8
+ import { Prompts } from './prompts.mjs'
7
9
 
8
10
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
9
11
  const payload = path.join(packageRoot, 'dist')
@@ -56,18 +58,132 @@ function installSkill (args, cwd) {
56
58
  return 0
57
59
  }
58
60
 
61
+ async function interactive (cwd, action) {
62
+ const prompts = new Prompts()
63
+ const dispatch = argv => { prompts.close(); return main(argv, cwd) }
64
+ const choices = pairs => pairs.map(([label, value]) => ({ label, value }))
65
+ const mode = async () => {
66
+ const value = await prompts.select('环境模式', choices([
67
+ ['使用工程默认环境', ''], ['dev', 'dev'], ['pre', 'pre'], ['ppe', 'ppe'], ['prd', 'prd'],
68
+ ['localize', 'localize'], ['development', 'development'], ['production', 'production'], ['输入其他模式', '*']
69
+ ]))
70
+ return value === '*' ? prompts.required('模式名称') : value
71
+ }
72
+ try {
73
+ console.log('@epxing/cli · 当前目录:' + cwd)
74
+ if (!action) {
75
+ action = await prompts.select('选择操作', choices([
76
+ ['dev 启动开发服务', 'dev'], ['build 工程打包', 'build'], ['lib 产品库 / 平台组件库打包', 'lib'],
77
+ ['preview 预览构建结果', 'preview'], ['create 创建产品 / 项目', 'create'], ['migrate 升级到 Vue 3', 'migrate'],
78
+ ['skill 安装升级技能', 'skill'], ['check 风险扫描 / 组件校验 / 代码检查', 'check'],
79
+ ['advanced 平台独立包 / 远程模块构建', 'advanced'], ['help 查看命令帮助', 'help'], ['exit 退出', 'exit']
80
+ ]))
81
+ }
82
+ if (action === 'exit') return 0
83
+ if (action === 'help') return dispatch(['help'])
84
+ if (action === 'create') {
85
+ const name = await prompts.required('产品名或目标路径')
86
+ const target = await prompts.required('目标 epx-frame 版本或包 spec')
87
+ const dir = await prompts.text('生成目录', name)
88
+ return dispatch(['create', name, '--dir', dir, '--epx-frame', target])
89
+ }
90
+ if (action === 'migrate') {
91
+ const dir = await prompts.required('待升级工程目录', '.')
92
+ const target = await prompts.required('目标 epx-frame 版本或包 spec')
93
+ const flags = await prompts.select('迁移范围', choices([
94
+ ['依赖与入口迁移', []], ['依赖、入口与 Vue 组件转换', ['--transform-vue']],
95
+ ['依赖、Vue 组件及 Vite 构建迁移', ['--transform-vue', '--to-vite']]
96
+ ]))
97
+ const args = ['migrate', dir, '--epx-frame', target, ...flags]
98
+ const preview = await main(args, cwd)
99
+ if (preview !== 0 || !(await prompts.confirm('应用以上迁移?要求 Git 工作树干净;默认不修改'))) return preview
100
+ return dispatch([...args, '--apply'])
101
+ }
102
+ if (action === 'skill') {
103
+ const dir = await prompts.required('安装到哪个项目', '.')
104
+ const location = await prompts.required('项目中的技能目录', '.claude/skills')
105
+ const dest = path.resolve(cwd, dir, location, skillName)
106
+ const args = ['skill', 'install', '--dir', dir, '--skills-dir', location]
107
+ if (['SKILL.md', 'pitfalls.md'].some(file => fs.existsSync(path.join(dest, file)))) {
108
+ if (!(await prompts.confirm(`技能已存在,替换 ${dest} 中的两个技能文件?`))) return 0
109
+ args.push('--force')
110
+ }
111
+ return dispatch(args)
112
+ }
113
+ if (action === 'check') {
114
+ action = await prompts.select('检查类型', choices([
115
+ ['源码迁移风险扫描', 'scan'], ['本地 / 线上组件清单核对', 'verify'], ['项目代码检查', 'lint'], ['项目循环依赖检查', 'circular']
116
+ ]))
117
+ }
118
+ if (action === 'advanced') {
119
+ action = await prompts.select('平台构建', choices([
120
+ ['business 远程模块', 'remote'], ['独立组件包', 'pkg']
121
+ ]))
122
+ }
123
+ const dir = await prompts.required('工程目录', '.')
124
+ if (action === 'scan' || action === 'verify') {
125
+ const online = action === 'verify' ? await prompts.text('线上 JS 地址或文件(留空仅列本地组件)') : ''
126
+ return dispatch([action, dir, ...(online ? ['--online', online] : [])])
127
+ }
128
+ const args = [action, '--dir', dir]
129
+ const project = findProject(path.resolve(cwd, dir))
130
+ const platformLib = action === 'lib' && project.scripts['build:component'] && !project.scripts['build:lib'] && !project.scripts.lib
131
+ if (['dev', 'build', 'lib', 'preview'].includes(action) && !platformLib) {
132
+ const selectedMode = await mode()
133
+ if (selectedMode) args.push('--mode', selectedMode)
134
+ }
135
+ if (action === 'dev' || action === 'preview') {
136
+ const port = await prompts.text('端口(留空使用工程默认值)')
137
+ if (port) args.push('--port', port)
138
+ }
139
+ if (action === 'build') {
140
+ if (project.pkg.name === 'epx-frame' || project.scripts['build:component']) {
141
+ if (await prompts.confirm('将 business 拆为远程模块,构建瘦平台?')) args.push('--thin')
142
+ if (await prompts.confirm('裁剪页面设计器?')) args.push('--no-designer')
143
+ if (await prompts.confirm('裁剪看板?')) args.push('--no-dashboard')
144
+ }
145
+ }
146
+ if (action === 'lib') {
147
+ if ((project.scripts['build:lib'] || project.scripts.lib) && await prompts.confirm('构建按需产品库?')) args.push('--lazy')
148
+ }
149
+ if (action === 'pkg') args.push(await prompts.required('包名或 all', 'all'))
150
+ if (action === 'lint' && await prompts.confirm('自动修复代码?')) args.push('--fix')
151
+ return dispatch(args)
152
+ } finally { prompts.close() }
153
+ }
154
+
59
155
  export async function main (argv = process.argv.slice(2), cwd = process.env.INIT_CWD || process.cwd()) {
60
- const [command, ...args] = argv
61
- if (!command || ['help', '--help', '-h'].includes(command)) {
62
- console.log(`@epxing/cli · 产品开发工具(独立于平台版本)
156
+ let [command, ...args] = argv
157
+ if (command === 'serve') command = 'dev'
158
+ if (command === 'upgrade') command = 'migrate'
159
+ const separator = args.indexOf('--')
160
+ if (args.slice(0, separator < 0 ? args.length : separator).some(arg => arg === '--help' || arg === '-h')) return main(['help'], cwd)
161
+ if (!command || command === 'menu') return interactive(cwd)
162
+ if (['create', 'migrate'].includes(command) && !args.length) return interactive(cwd, command)
163
+ if (['dev', 'build', 'lib', 'preview', 'remote', 'pkg', 'lint', 'circular'].includes(command)) return runProject(command, args, cwd)
164
+ if (['help', '--help', '-h'].includes(command)) {
165
+ console.log(`@epxing/cli · 工程开发与升级工具(独立于平台版本)
63
166
 
167
+ epx / epx menu 交互菜单(↑↓ + Enter;管道支持数字输入)
168
+ epx dev [-m <模式>] [-p <端口>] [--dir <工程目录>]
169
+ epx build [-m <模式>] [--thin] [--no-designer] [--no-dashboard] [--dir <目录>]
170
+ epx lib [--lazy] [-m <模式>] [--dir <目录>] 产品 build:lib / 平台 build:component
171
+ epx build --lib 同 epx lib
172
+ epx preview [-p <端口>] [--dir <目录>]
64
173
  epx create <名称|路径> --epx-frame <目标版本> [--dir <目录>]
65
174
  epx migrate [项目目录] --epx-frame <目标版本> [--apply] [--transform-vue] [--to-vite]
66
175
  epx scan [项目目录]
67
176
  epx verify [项目目录] [--online <线上JS地址|文件>]
68
177
  epx skill install [--dir <项目目录>] [--skills-dir <技能目录>] [--force]
178
+ epx lint [--fix] [--dir <目录>]
179
+ epx circular [--dir <目录>]
180
+ epx remote [--dir <平台目录>]
181
+ epx pkg <包名|all> [--dir <平台目录>]
69
182
  epx --version
70
183
 
184
+ dev / build / lib 等执行当前工程的 package.json 脚本,使用工程本地依赖。
185
+ 工程命令可用 -- 后的参数透传给脚本;serve / upgrade 分别是 dev / migrate 的别名。
186
+ create、migrate 不带参数时进入引导;直接带参数时 --epx-frame 必填。
71
187
  创建和升级自动写入 runtime: "vue3"。迁移默认 dry-run,--apply 才落盘。
72
188
  工具不会自动安装平台依赖;.vue 转换工具仅在执行 --apply --transform-vue 时下载并缓存。`)
73
189
  return 0
@@ -104,11 +220,11 @@ export async function main (argv = process.argv.slice(2), cwd = process.env.INIT
104
220
  const script = command === 'scan' ? 'detect-vue3-risks.mjs' : 'verify-components.mjs'
105
221
  return runTool('epx-migrate/' + script, [path.resolve(cwd, args[0] || '.'), ...(online ? ['--online', online] : [])], cwd)
106
222
  }
107
- throw new Error(`未知命令:${command},运行 epx help 查看用法。平台构建命令仍在框架仓库使用 pnpm epx。`)
223
+ throw new Error(`未知命令:${command},运行 epx help 查看用法。`)
108
224
  }
109
225
 
110
226
  let isMain = false
111
227
  try { isMain = !!process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url) } catch (error) { /* 作为模块导入 */ }
112
228
  if (isMain) {
113
- main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = 1 })
229
+ main().then(status => { process.exitCode = status }).catch(error => { console.error('✗ ' + error.message); process.exitCode = error.exitCode || 1 })
114
230
  }
@@ -0,0 +1,178 @@
1
+ /* eslint-disable no-console -- 输出项目命令 */
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { createRequire } from 'node:module'
5
+ import { spawn } from 'node:child_process'
6
+
7
+ export function findProject (cwd) {
8
+ let dir = path.resolve(cwd)
9
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new Error(`工程目录不存在:${dir}`)
10
+ while (true) {
11
+ const file = path.join(dir, 'package.json')
12
+ if (fs.existsSync(file)) {
13
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'))
14
+ return { dir, pkg, scripts: pkg.scripts || {} }
15
+ }
16
+ const parent = path.dirname(dir)
17
+ if (parent === dir) throw new Error(`未找到项目 package.json:${cwd}。请进入项目目录,或指定 --dir。`)
18
+ dir = parent
19
+ }
20
+ }
21
+
22
+ function npmEntry () {
23
+ const nodeDir = path.dirname(process.execPath)
24
+ const roots = [nodeDir, ...String(process.env.PATH || process.env.Path || '').split(path.delimiter)]
25
+ const candidates = [process.env.npm_execpath && /npm-cli\.js$/.test(process.env.npm_execpath) ? process.env.npm_execpath : null]
26
+ for (const dir of roots) {
27
+ if (!dir) continue
28
+ candidates.push(path.join(dir, 'node_modules/npm/bin/npm-cli.js'), path.resolve(dir, '../lib/node_modules/npm/bin/npm-cli.js'))
29
+ const executable = path.join(dir, 'npm')
30
+ try {
31
+ const real = fs.realpathSync(executable)
32
+ if (real.endsWith('.js')) candidates.push(real)
33
+ } catch (error) { /* 继续查找 */ }
34
+ }
35
+ const found = candidates.find(file => file && fs.existsSync(file))
36
+ if (!found) throw new Error('未找到 npm-cli.js。请使用带 npm 的 Node.js 环境运行 epx。')
37
+ return found
38
+ }
39
+
40
+ function execute (file, args, cwd, extraEnv) {
41
+ return new Promise((resolve, reject) => {
42
+ const child = spawn(process.execPath, [file, ...args], { cwd, stdio: 'inherit', env: { ...process.env, ...extraEnv } })
43
+ const interrupt = () => child.kill('SIGINT')
44
+ const terminate = () => child.kill('SIGTERM')
45
+ const cleanup = () => {
46
+ process.removeListener('SIGINT', interrupt)
47
+ process.removeListener('SIGTERM', terminate)
48
+ }
49
+ process.on('SIGINT', interrupt)
50
+ process.on('SIGTERM', terminate)
51
+ child.once('error', error => { cleanup(); reject(error) })
52
+ child.once('exit', (code, signal) => { cleanup(); resolve(code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1)) })
53
+ })
54
+ }
55
+
56
+ function localBinary (project, name) {
57
+ const require = createRequire(path.join(project.dir, 'package.json'))
58
+ try {
59
+ const manifest = require.resolve(name + '/package.json')
60
+ const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'))
61
+ const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[name]
62
+ const file = bin && path.resolve(path.dirname(manifest), bin)
63
+ if (file && fs.existsSync(file)) return file
64
+ } catch (error) { /* 由调用方提示项目安装依赖 */ }
65
+ return null
66
+ }
67
+
68
+ function takeValue (args, flag, short) {
69
+ const index = args.findIndex(arg => arg === flag || arg.startsWith(flag + '=') || (short && (arg === short || arg.startsWith(short))))
70
+ if (index < 0) return null
71
+ const arg = args[index]
72
+ let value
73
+ if (arg.startsWith(flag + '=')) { value = arg.slice(flag.length + 1); args.splice(index, 1) } else if (short && arg.startsWith(short) && arg !== short) { value = arg.slice(short.length); args.splice(index, 1) } else {
74
+ value = args[index + 1]
75
+ if (!value || value.startsWith('-')) throw new Error(`${flag} 缺少值`)
76
+ args.splice(index, 2)
77
+ }
78
+ if (!value) throw new Error(`${flag} 缺少值`)
79
+ return value
80
+ }
81
+
82
+ export function parseProjectArgs (argv) {
83
+ const args = [...argv]
84
+ const separator = args.indexOf('--')
85
+ const forward = separator >= 0 ? args.splice(separator).slice(1) : []
86
+ const flag = name => { const index = args.indexOf(name); if (index < 0) return false; args.splice(index, 1); return true }
87
+ const result = {
88
+ dir: takeValue(args, '--dir'),
89
+ mode: takeValue(args, '--mode', '-m'),
90
+ port: takeValue(args, '--port', '-p'),
91
+ thin: flag('--thin'),
92
+ noDesigner: flag('--no-designer'),
93
+ noDashboard: flag('--no-dashboard'),
94
+ lazy: flag('--lazy'),
95
+ lib: flag('--lib'),
96
+ fix: flag('--fix'),
97
+ forward,
98
+ positional: args
99
+ }
100
+ if (result.mode && !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(result.mode)) throw new Error('mode 仅支持字母、数字、点、下划线和短横线')
101
+ if (result.port && (!/^\d+$/.test(result.port) || +result.port < 1 || +result.port > 65535)) throw new Error('端口需为 1–65535 的整数')
102
+ return result
103
+ }
104
+
105
+ export function projectPlan (command, options, project) {
106
+ const { scripts } = project
107
+ const pick = (...names) => names.find(name => name && typeof scripts[name] === 'string' && scripts[name].trim())
108
+ const args = []
109
+ const env = {}
110
+ let script
111
+ let binary
112
+ const platform = project.pkg.name === 'epx-frame' || !!scripts['build:component']
113
+ if (options.lib) {
114
+ if (command !== 'build') throw new Error('--lib 用于 epx build;也可直接使用 epx lib')
115
+ command = 'lib'
116
+ }
117
+ if (options.positional.length && !(command === 'pkg' && options.positional.length === 1 && !options.positional[0].startsWith('-'))) throw new Error(`不支持的参数:${options.positional.join(' ')}`)
118
+ if (options.port && !['dev', 'preview'].includes(command)) throw new Error('--port 仅用于 dev 或 preview')
119
+ if (options.lazy && command !== 'lib') throw new Error('--lazy 仅用于 lib 或 build --lib')
120
+ if (options.fix && command !== 'lint') throw new Error('--fix 仅用于 lint')
121
+ if ((options.thin || options.noDesigner || options.noDashboard) && (command !== 'build' || !platform)) throw new Error('平台裁剪选项仅用于平台源码中的 epx build')
122
+ if (options.mode && !['dev', 'build', 'lib', 'preview'].includes(command)) throw new Error('--mode 仅用于 dev、build、lib 或 preview')
123
+ if (command === 'dev') {
124
+ script = pick('dev', 'serve', 'start')
125
+ if (!script) binary = 'vite'
126
+ } else if (command === 'build') {
127
+ const modeScript = options.mode && pick('build:' + options.mode, options.mode === 'localize' && 'build:local')
128
+ script = modeScript || pick('build', 'build:lib')
129
+ if (!script) binary = 'vite'
130
+ if (modeScript) options = { ...options, mode: null }
131
+ if (options.thin) env.EXTERNALIZE_BUSINESS = 'true'
132
+ if (options.noDesigner) env.INCLUDE_DESIGNER = 'false'
133
+ if (options.noDashboard) env.INCLUDE_DASHBOARD = 'false'
134
+ } else if (command === 'lib') {
135
+ script = options.lazy ? pick('build:lib:lazy') : pick('build:lib', 'lib', 'build:component')
136
+ if (options.lazy && !script) {
137
+ script = pick('build:lib', 'lib')
138
+ env.LAZY_LIB = 'true'
139
+ }
140
+ if (!script && !options.lazy && /--target(?:=|\s+)lib\b/.test(scripts.build || '')) script = 'build'
141
+ if (!script) throw new Error('当前工程缺少库构建脚本。产品请提供 build:lib(按需库可用 build:lib:lazy),平台使用 build:component。')
142
+ if (script === 'build:component' && options.mode) throw new Error('平台聚合库包含多步构建,请在 build:component 中配置环境;epx lib 不向该聚合脚本追加 --mode。')
143
+ } else if (command === 'preview') {
144
+ script = pick('preview', 'vite:preview')
145
+ if (!script) binary = 'vite'
146
+ } else if (command === 'remote') script = pick('build:remote')
147
+ else if (command === 'pkg') {
148
+ const name = options.positional[0]
149
+ if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) throw new Error('用法:epx pkg <包名|all> [--dir <平台目录>]')
150
+ script = name === 'all' ? pick('build:pkgs') : pick('build:pkg')
151
+ if (name !== 'all') env.PKG = name
152
+ } else if (command === 'lint') {
153
+ script = options.fix ? pick('lint:fix', 'lint') : pick('lint')
154
+ if (options.fix && script !== 'lint:fix') args.push('--fix')
155
+ } else if (command === 'circular') script = pick('circular')
156
+ if (!script && !binary) throw new Error(`当前工程未提供 ${command} 对应的 package.json 脚本`)
157
+ if (options.mode) args.push('--mode', options.mode)
158
+ if (options.port) args.push('--port', options.port)
159
+ args.push(...options.forward)
160
+ if (['build', 'lib', 'remote', 'pkg'].includes(command) && !process.env.NODE_OPTIONS) env.NODE_OPTIONS = '--max-old-space-size=8192'
161
+ return { command, script, binary, args, env }
162
+ }
163
+
164
+ export async function runProject (command, argv, cwd) {
165
+ const options = parseProjectArgs(argv)
166
+ const project = findProject(options.dir ? path.resolve(cwd, options.dir) : cwd)
167
+ const plan = projectPlan(command, options, project)
168
+ console.log(`项目:${project.pkg.name || project.dir}(${project.dir})`)
169
+ if (plan.script) {
170
+ console.log(`→ npm run ${plan.script}${plan.args.length ? ' -- ' + plan.args.join(' ') : ''}`)
171
+ return execute(npmEntry(), ['run', plan.script, ...(plan.args.length ? ['--', ...plan.args] : [])], project.dir, plan.env)
172
+ }
173
+ const file = localBinary(project, plan.binary)
174
+ if (!file) throw new Error(`当前工程没有可用脚本或本地 ${plan.binary},请先在工程内安装依赖并配置相应脚本。`)
175
+ const args = [...(command === 'dev' ? [] : [command]), ...plan.args]
176
+ console.log(`→ 本地 ${plan.binary} ${args.join(' ')}`)
177
+ return execute(file, args, project.dir, plan.env)
178
+ }
@@ -0,0 +1,86 @@
1
+ import readline from 'node:readline'
2
+
3
+ export class Cancelled extends Error {
4
+ constructor () { super('已取消'); this.exitCode = 130 }
5
+ }
6
+
7
+ // 非 TTY 共用一个输入队列,避免多步问答丢掉管道中提前到达的行。
8
+ export class Prompts {
9
+ constructor (input = process.stdin, output = process.stdout) {
10
+ this.input = input
11
+ this.output = output
12
+ this.tty = !!(input.isTTY && output.isTTY && typeof input.setRawMode === 'function')
13
+ this.lines = []
14
+ this.closed = false
15
+ if (!this.tty) {
16
+ this.rl = readline.createInterface({ input, terminal: false })
17
+ this.rl.on('line', line => {
18
+ if (this.pending) { const pending = this.pending; this.pending = null; pending.resolve(line) } else this.lines.push(line)
19
+ })
20
+ this.rl.on('close', () => { this.closed = true; if (this.pending) { this.pending.reject(new Cancelled()); this.pending = null } })
21
+ }
22
+ }
23
+
24
+ close () { this.rl?.close() }
25
+
26
+ line () {
27
+ if (this.lines.length) return Promise.resolve(this.lines.shift())
28
+ if (this.closed) return Promise.reject(new Cancelled())
29
+ return new Promise((resolve, reject) => { this.pending = { resolve, reject } })
30
+ }
31
+
32
+ async text (question, defaultValue = '') {
33
+ const prompt = question + (defaultValue ? `(回车用 ${defaultValue})` : '') + ':'
34
+ let answer
35
+ if (!this.tty) { this.output.write(prompt); answer = await this.line() } else {
36
+ answer = await new Promise((resolve, reject) => {
37
+ let settled = false
38
+ const rl = readline.createInterface({ input: this.input, output: this.output })
39
+ rl.once('close', () => { if (!settled) reject(new Cancelled()) })
40
+ rl.once('SIGINT', () => { settled = true; rl.close(); reject(new Cancelled()) })
41
+ rl.question(prompt, value => { settled = true; rl.close(); resolve(value) })
42
+ })
43
+ }
44
+ return answer.trim() || defaultValue
45
+ }
46
+
47
+ async required (question, defaultValue = '') {
48
+ while (true) { const value = await this.text(question, defaultValue); if (value) return value; this.output.write('此项不能为空。\n') }
49
+ }
50
+
51
+ async select (question, choices) {
52
+ if (!this.tty) {
53
+ this.output.write(question + '\n' + choices.map((choice, index) => ` ${index + 1}) ${choice.label}`).join('\n') + '\n')
54
+ while (true) {
55
+ const answer = await this.text('输入序号', '1')
56
+ if (/^\d+$/.test(answer) && choices[+answer - 1]) return choices[+answer - 1].value
57
+ this.output.write('请选择有效序号。\n')
58
+ }
59
+ }
60
+ return new Promise((resolve, reject) => {
61
+ let index = 0
62
+ let drawn = false
63
+ const draw = () => {
64
+ if (drawn) this.output.write(`\x1b[${choices.length + 1}A\x1b[0J`)
65
+ this.output.write(question + '(↑↓ 选择,Enter 确认,Esc 取消)\n')
66
+ choices.forEach((choice, i) => this.output.write(i === index ? `\x1b[36m❯ ${choice.label}\x1b[0m\n` : ` ${choice.label}\n`))
67
+ drawn = true
68
+ }
69
+ const cleanup = () => { this.input.removeListener('keypress', onKey); this.input.setRawMode(false); this.input.pause() }
70
+ const onKey = (text, key) => {
71
+ if (!key) return
72
+ if (key.name === 'up' || key.name === 'k') { index = (index - 1 + choices.length) % choices.length; draw() } else if (key.name === 'down' || key.name === 'j') { index = (index + 1) % choices.length; draw() } else if (key.name === 'return' || key.name === 'enter') { cleanup(); resolve(choices[index].value) } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { cleanup(); this.output.write('\n'); reject(new Cancelled()) }
73
+ }
74
+ readline.emitKeypressEvents(this.input)
75
+ this.input.setRawMode(true)
76
+ this.input.resume()
77
+ this.input.on('keypress', onKey)
78
+ draw()
79
+ })
80
+ }
81
+
82
+ confirm (question, defaultValue = false) {
83
+ const choices = [{ label: '否', value: false }, { label: '是', value: true }]
84
+ return this.select(question, defaultValue ? choices.reverse() : choices)
85
+ }
86
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@epxing/cli",
3
- "version": "0.1.0",
4
- "description": "EPX 产品和项目创建、Vue3 迁移及升级技能安装工具",
3
+ "version": "0.2.0",
4
+ "description": "EPX 交互式工程开发、服务启动、构建、Vue3 迁移及技能安装工具",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "epx": "dist/cli.mjs"