uniner-cli 1.0.5 → 1.0.7
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 +78 -44
- package/bin/uniner-cli-uninstall.js +66 -127
- package/bin/uniner-cli.js +195 -3
- package/lib/SKILL.md.template +19 -17
- package/package.json +4 -3
- package/scripts/cleanup.js +123 -0
- package/scripts/postinstall.js +168 -0
- package/scripts/preuninstall.js +25 -0
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`uniner-cli` 是 [Uniner](https://www.uniner.com/) 的 RPA 自动化命令行工具,提供:
|
|
4
4
|
|
|
5
|
-
- **Web
|
|
5
|
+
- **Web 自动化** —— 浏览器抓取、表单填写、元素交互
|
|
6
6
|
- **Excel 操作** —— 读写、批量处理、样式
|
|
7
|
-
- **OCR 识别** ——
|
|
8
|
-
- **桌面 / SAP
|
|
7
|
+
- **OCR 识别** —— 支持验证码和通用 OCR
|
|
8
|
+
- **桌面 / SAP 自动化** —— 支持桌面软件的自动化(仅 Windows)
|
|
9
9
|
- **通用工具** —— 加密、HTTP、邮件、文件、PDF、压缩
|
|
10
10
|
|
|
11
11
|
安装时会自动注册一个 [Claude Code](https://claude.com/claude-code) 技能(`~/.claude/skills/uniner-cli/SKILL.md`),让 Claude 一启动就能识别并调用本 CLI。
|
|
@@ -14,18 +14,27 @@
|
|
|
14
14
|
|
|
15
15
|
## 安装
|
|
16
16
|
|
|
17
|
+
全局安装:
|
|
18
|
+
|
|
17
19
|
```bash
|
|
18
20
|
npm install -g uniner-cli
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
当前目录安装:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install uniner-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> 本地安装后通常通过 `npx uniner-cli --help` 或项目里的 npm scripts 调用;只有全局安装才适合在任意终端直接运行 `uniner-cli`。
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
2. `postinstall` 钩子从 `static.uniner.com` 拉取约 250MB 便携包
|
|
25
|
-
3. 解压到 `%USERPROFILE%\.uniner-cli\<version>\`
|
|
26
|
-
4. 写入 `%USERPROFILE%\.claude\skills\uniner-cli\SKILL.md`
|
|
31
|
+
安装过程中会自动完成:
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
1. 下载并配置当前平台所需的 uniner-cli 运行时
|
|
34
|
+
2. 注册 Claude Code 技能
|
|
35
|
+
3. 准备 `uniner-cli` 命令入口
|
|
36
|
+
|
|
37
|
+
全局安装完成后任意终端:
|
|
29
38
|
|
|
30
39
|
```bash
|
|
31
40
|
uniner-cli --help
|
|
@@ -35,12 +44,12 @@ uniner-cli --help
|
|
|
35
44
|
|
|
36
45
|
| 项 | 要求 |
|
|
37
46
|
|---|---|
|
|
38
|
-
| 操作系统 |
|
|
47
|
+
| 操作系统 | Windows 10/11 x64 · macOS 12+ ARM64 · Linux x64 |
|
|
39
48
|
| Node.js | >= 14 |
|
|
40
|
-
| 磁盘空间 | ≥
|
|
49
|
+
| 磁盘空间 | ≥ 500 MB(单平台) |
|
|
41
50
|
| 网络 | 能访问 `static.uniner.com` |
|
|
42
51
|
|
|
43
|
-
>
|
|
52
|
+
> 安装脚本会自动检测当前系统并下载对应版本。
|
|
44
53
|
|
|
45
54
|
## 使用示例
|
|
46
55
|
|
|
@@ -61,7 +70,7 @@ uniner-cli excel data write --file out.xlsx --cell B2 --value "hello"
|
|
|
61
70
|
uniner-cli tool ocr ddddocr --img captcha.png
|
|
62
71
|
```
|
|
63
72
|
|
|
64
|
-
完整命令树(一级 + 二级 +
|
|
73
|
+
完整命令树(一级 + 二级 + 三级参数)可通过 `uniner-cli --help` 查看。
|
|
65
74
|
|
|
66
75
|
## 升级
|
|
67
76
|
|
|
@@ -79,24 +88,45 @@ npm install -g uniner-cli@latest
|
|
|
79
88
|
|
|
80
89
|
## 卸载
|
|
81
90
|
|
|
91
|
+
> **重要提示**:直接使用 `npm uninstall` 可能无法清理运行时文件。
|
|
92
|
+
> 请使用 CLI 自带的一键卸载命令以确保完全清理。
|
|
93
|
+
|
|
94
|
+
**推荐卸载流程**:
|
|
95
|
+
|
|
82
96
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
uniner-cli uninstall
|
|
98
|
+
```
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
该命令会先清理运行时残留,再自动判断安装范围并移除 npm 包本体:
|
|
101
|
+
|
|
102
|
+
- 全局安装:执行 `npm uninstall -g uniner-cli`
|
|
103
|
+
- 本地安装:在当前项目执行 `npm uninstall uniner-cli`
|
|
104
|
+
|
|
105
|
+
如果自动判断不符合预期,可显式指定:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uniner-cli uninstall --global
|
|
109
|
+
uniner-cli uninstall --local
|
|
88
110
|
```
|
|
89
111
|
|
|
90
|
-
|
|
112
|
+
清理内容:
|
|
113
|
+
|
|
114
|
+
- `~/.uniner-cli/` 整个运行时目录(含所有版本)
|
|
115
|
+
- `~/.claude/skills/uniner-cli/SKILL.md` 技能文件
|
|
91
116
|
|
|
92
|
-
|
|
117
|
+
> `~/.claude/skills/uniner-cli/scenarios/` 下用户积累的场景数据不会被删除,需手动清理。
|
|
118
|
+
|
|
119
|
+
**备选方式**(如果 npm 包已经先被卸载,`uniner-cli` 不在 PATH 上):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node ~/.uniner-cli/cleanup.js
|
|
123
|
+
```
|
|
93
124
|
|
|
94
|
-
|
|
95
|
-
- `%USERPROFILE%\.claude\skills\uniner-cli\SKILL.md` 技能
|
|
125
|
+
> 该脚本只清理运行时残留,不再卸载 npm 包本体。
|
|
96
126
|
|
|
97
127
|
## 环境变量
|
|
98
128
|
|
|
99
|
-
|
|
129
|
+
无需手动配置。全局安装时,npm 会把命令链接到全局 bin 目录;如果终端识别不到 `uniner-cli`,按下方排查 PATH。
|
|
100
130
|
|
|
101
131
|
## 故障排查
|
|
102
132
|
|
|
@@ -104,13 +134,19 @@ npm uninstall -g uniner-cli
|
|
|
104
134
|
|
|
105
135
|
确认 npm 全局 bin 在 PATH 上:
|
|
106
136
|
|
|
107
|
-
```
|
|
137
|
+
```bash
|
|
138
|
+
# macOS / Linux
|
|
139
|
+
npm config get prefix
|
|
140
|
+
echo $PATH | tr ':' '\n' | grep npm
|
|
141
|
+
|
|
142
|
+
# Windows (PowerShell)
|
|
108
143
|
npm config get prefix
|
|
109
|
-
# 应输出类似 C:\Users\xxx\AppData\Roaming\npm
|
|
110
144
|
$env:Path -split ';' | Where-Object { $_ -like '*npm*' }
|
|
111
145
|
```
|
|
112
146
|
|
|
113
|
-
|
|
147
|
+
如果没有,手动将 npm 全局 bin 目录加入 PATH:
|
|
148
|
+
- macOS / Linux: `$(npm config get prefix)/bin`
|
|
149
|
+
- Windows: `%APPDATA%\npm`
|
|
114
150
|
|
|
115
151
|
### 安装时下载失败 / 超时
|
|
116
152
|
|
|
@@ -123,39 +159,37 @@ npm config get https-proxy
|
|
|
123
159
|
npm config set https-proxy http://your-proxy:port
|
|
124
160
|
```
|
|
125
161
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```powershell
|
|
129
|
-
iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing | iex
|
|
130
|
-
```
|
|
162
|
+
### 安装卡住 / 解压慢
|
|
131
163
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
杀软(360 / 火绒)会扫描 1279 个新文件,慢则 5–10 分钟。让它跑完即可。
|
|
164
|
+
杀软(360 / 火绒)可能会扫描大量新文件,慢则 5–10 分钟。让它跑完即可。
|
|
135
165
|
|
|
136
166
|
### 想重装
|
|
137
167
|
|
|
138
168
|
```bash
|
|
139
|
-
|
|
169
|
+
uniner-cli uninstall
|
|
140
170
|
npm install -g uniner-cli
|
|
141
171
|
```
|
|
142
172
|
|
|
143
|
-
## 备选安装方式(不走 npm)
|
|
144
|
-
|
|
145
|
-
如果你不想要 Node.js 依赖,可以用 PowerShell 一行安装(基于 Inno Setup 安装器):
|
|
146
|
-
|
|
147
|
-
```powershell
|
|
148
|
-
iwr https://static.uniner.com/soft/editor/uninercli/install.ps1 -UseBasicParsing | iex
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
两种方式互不干扰,但**不要**同时启用——会有两份 SKILL.md / 两份二进制。
|
|
152
|
-
|
|
153
173
|
## 链接
|
|
154
174
|
|
|
155
175
|
- 官网:[https://www.uniner.com/](https://www.uniner.com/)
|
|
156
176
|
- 技术支持:support@uniner.com
|
|
157
177
|
- Claude Code:[https://claude.com/claude-code](https://claude.com/claude-code)
|
|
158
178
|
|
|
179
|
+
## Changelog
|
|
180
|
+
|
|
181
|
+
### 1.0.7 (2026-06-29)
|
|
182
|
+
|
|
183
|
+
- **fix**: 新增 `uniner-cli uninstall`,自动判断全局 / 本地安装范围,完整清理运行时文件并移除 npm 包。
|
|
184
|
+
|
|
185
|
+
### 1.0.6 (2026-06-28)
|
|
186
|
+
|
|
187
|
+
- **fix**: 修复 `npm uninstall -g uniner-cli` 未清理 `~/.uniner-cli/` 和 `~/.claude/skills/uniner-cli/SKILL.md` 的问题。
|
|
188
|
+
|
|
189
|
+
### 1.0.5 (2026-06-27)
|
|
190
|
+
|
|
191
|
+
- 首个公开版本,支持 Windows 10/11 (x64)、macOS (Apple Silicon)、Linux (x64)
|
|
192
|
+
|
|
159
193
|
## License
|
|
160
194
|
|
|
161
195
|
Proprietary. (C) 2026 Uniner Tech.
|
|
@@ -1,145 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// uniner-cli-uninstall
|
|
2
|
+
// uniner-cli-uninstall — manual cleanup command.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Background:
|
|
5
|
+
// npm 7+ (including npm 11) does NOT reliably run the `preuninstall`
|
|
6
|
+
// lifecycle hook for globally installed packages. This means
|
|
7
|
+
// `npm uninstall -g uniner-cli` removes the npm package but leaves
|
|
8
|
+
// residual files behind:
|
|
9
|
+
// - ~/.uniner-cli/ (runtime binaries, skills, docs)
|
|
10
|
+
// - ~/.claude/skills/uniner-cli/SKILL.md (Claude Code skill file)
|
|
7
11
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
12
|
+
// To solve this, postinstall writes a self-contained cleanup script to
|
|
13
|
+
// ~/.uniner-cli/cleanup.js. That file survives `npm uninstall` and can
|
|
14
|
+
// always be run manually.
|
|
15
|
+
//
|
|
16
|
+
// Recommended workflow (ensures a clean uninstall):
|
|
17
|
+
// npm uninstall -g uniner-cli
|
|
18
|
+
// node ~/.uniner-cli/cleanup.js # always works, even after npm removes the package
|
|
19
|
+
//
|
|
20
|
+
// Alternative (only works BEFORE you run `npm uninstall -g`):
|
|
21
|
+
// uniner-cli-uninstall # this command; removed along with the package
|
|
22
|
+
// npm uninstall -g uniner-cli
|
|
11
23
|
//
|
|
12
24
|
// Idempotent: if files are already gone, just no-ops.
|
|
13
25
|
|
|
14
26
|
'use strict';
|
|
15
27
|
|
|
16
|
-
const fs = require('fs');
|
|
17
28
|
const path = require('path');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const fs = require('fs');
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
fs.rmSync(p, { recursive: true, force: true });
|
|
34
|
-
log(`Removed ${p}`);
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
return false;
|
|
38
|
-
} catch (e) {
|
|
39
|
-
warn(`Could not remove ${p}: ${e.message}`);
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function rmFile(p) {
|
|
45
|
-
try {
|
|
46
|
-
if (fs.existsSync(p)) {
|
|
47
|
-
fs.unlinkSync(p);
|
|
48
|
-
log(`Removed ${p}`);
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
} catch (e) {
|
|
53
|
-
warn(`Could not remove ${p}: ${e.message}`);
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
32
|
+
// Prefer the standalone cleanup.js if it exists (written by postinstall,
|
|
33
|
+
// survives npm uninstall). Fall back to the in-package cleanup module
|
|
34
|
+
// (only works while the npm package is still installed).
|
|
35
|
+
const standalonePath = path.join(os.homedir(), '.uniner-cli', 'cleanup.js');
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(standalonePath)) {
|
|
38
|
+
// Delegate to the standalone script. It handles everything: removing
|
|
39
|
+
// ~/.uniner-cli/, removing SKILL.md, preserving scenarios/.
|
|
40
|
+
// Use spawnSync to avoid any require() path issues.
|
|
41
|
+
const { spawnSync } = require('child_process');
|
|
42
|
+
const result = spawnSync(process.execPath, [standalonePath], {
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
});
|
|
45
|
+
process.exit(result.status || 0);
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log(' uniner-cli uninstall cleanup');
|
|
62
|
-
console.log('==========================================');
|
|
63
|
-
|
|
64
|
-
const myVersion = pkg.version;
|
|
65
|
-
const myVersionDir = versionDir(myVersion);
|
|
66
|
-
let didSomething = false;
|
|
67
|
-
|
|
68
|
-
// 1. Remove our version's runtime tree.
|
|
69
|
-
if (rmRf(myVersionDir)) didSomething = true;
|
|
70
|
-
|
|
71
|
-
// 2. If ~/.uniner-cli/ has no other versions, drop the whole directory.
|
|
72
|
-
try {
|
|
73
|
-
const root = rootDir();
|
|
74
|
-
if (fs.existsSync(root)) {
|
|
75
|
-
const remaining = fs.readdirSync(root);
|
|
76
|
-
if (remaining.length === 0) {
|
|
77
|
-
fs.rmdirSync(root);
|
|
78
|
-
log(`Removed empty ${root}`);
|
|
79
|
-
didSomething = true;
|
|
80
|
-
} else {
|
|
81
|
-
log(`Other uniner-cli versions still installed: ${remaining.join(', ')}`);
|
|
82
|
-
log(`Keeping ${root}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} catch (e) {
|
|
86
|
-
warn(`Could not inspect ${rootDir()}: ${e.message}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 3. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir,
|
|
90
|
-
// because the user's accumulated scenarios/<name>/ subdirectories
|
|
91
|
-
// live there and represent real work they've sedimented over time.
|
|
92
|
-
// If they really want everything gone they can do it manually; we
|
|
93
|
-
// surface the path below so they know where to look.
|
|
94
|
-
const dir = skillDir();
|
|
95
|
-
if (rmFile(skillFile())) didSomething = true;
|
|
48
|
+
// Fallback: the standalone script is gone (old install before this fix).
|
|
49
|
+
// Use the in-package cleanup module.
|
|
50
|
+
'use strict';
|
|
96
51
|
|
|
97
|
-
|
|
98
|
-
let scenariosKept = 0;
|
|
99
|
-
const scenariosDir = path.join(dir, 'scenarios');
|
|
100
|
-
if (fs.existsSync(scenariosDir)) {
|
|
101
|
-
try {
|
|
102
|
-
scenariosKept = fs.readdirSync(scenariosDir).filter(n => {
|
|
103
|
-
const meta = path.join(scenariosDir, n, 'meta.json');
|
|
104
|
-
return fs.existsSync(meta);
|
|
105
|
-
}).length;
|
|
106
|
-
} catch (_) {
|
|
107
|
-
// best-effort
|
|
108
|
-
}
|
|
109
|
-
}
|
|
52
|
+
const { cleanup } = require('../scripts/cleanup');
|
|
110
53
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} catch (_) {
|
|
122
|
-
// best-effort
|
|
123
|
-
}
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log('==========================================');
|
|
56
|
+
console.log(' uniner-cli uninstall cleanup (fallback)');
|
|
57
|
+
console.log('==========================================');
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(' Note: ~/.uniner-cli/cleanup.js not found.');
|
|
60
|
+
console.log(' This fallback will be removed in a future version.');
|
|
61
|
+
console.log(' Reinstall to get the standalone cleanup script:');
|
|
62
|
+
console.log(' npm install -g uniner-cli');
|
|
63
|
+
console.log('');
|
|
124
64
|
|
|
125
|
-
|
|
126
|
-
log('Nothing to clean — already removed.');
|
|
127
|
-
}
|
|
65
|
+
const result = cleanup({ verbose: true });
|
|
128
66
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
console.log('Cleanup done. To complete uninstall, also run:');
|
|
141
|
-
console.log(' npm uninstall -g uniner-cli');
|
|
142
|
-
console.log('');
|
|
67
|
+
console.log('');
|
|
68
|
+
if (result.scenariosKept > 0) {
|
|
69
|
+
console.log('==========================================');
|
|
70
|
+
console.log(` ${result.scenariosKept} scenario(s) PRESERVED at:`);
|
|
71
|
+
console.log(` ${result.scenariosDir}`);
|
|
72
|
+
console.log(' (your accumulated knowledge — not deleted)');
|
|
73
|
+
console.log(' To delete them too:');
|
|
74
|
+
console.log(` rm -rf "${result.scenariosDir}" # bash`);
|
|
75
|
+
console.log(` Remove-Item "${result.scenariosDir}" -Recurse -Force # PowerShell`);
|
|
76
|
+
console.log('==========================================');
|
|
143
77
|
}
|
|
144
78
|
|
|
145
|
-
|
|
79
|
+
if (result.didSomething) {
|
|
80
|
+
console.log('Cleanup done.');
|
|
81
|
+
} else {
|
|
82
|
+
console.log('Nothing to clean — already removed.');
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
package/bin/uniner-cli.js
CHANGED
|
@@ -1,15 +1,207 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Tiny launcher:
|
|
2
|
+
// Tiny launcher: handle npm-owned commands, then forward everything else to
|
|
3
|
+
// the real PyInstaller exe.
|
|
3
4
|
//
|
|
4
5
|
// `npm install -g uniner-cli` registers this file as the global `uniner-cli`
|
|
5
|
-
// command (because of the `bin` entry in package.json).
|
|
6
|
-
// binary with the same argv/stdio and exit with its status code.
|
|
6
|
+
// command (because of the `bin` entry in package.json).
|
|
7
7
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const { spawnSync } = require('child_process');
|
|
11
11
|
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
12
13
|
const { realBinaryPath, isInstalled, pkg } = require('../lib/paths');
|
|
14
|
+
const { cleanup } = require('../scripts/cleanup');
|
|
15
|
+
|
|
16
|
+
function npmCommand() {
|
|
17
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printUninstallHelp() {
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log('Usage: uniner-cli uninstall [--global|--local]');
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log('Cleans ~/.uniner-cli and the installed Claude Code skill,');
|
|
25
|
+
console.log('then removes this npm package from the detected install scope.');
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function realPath(p) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.realpathSync.native(p);
|
|
32
|
+
} catch (_) {
|
|
33
|
+
return path.resolve(p);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function samePath(a, b) {
|
|
38
|
+
const left = realPath(a);
|
|
39
|
+
const right = realPath(b);
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
return left === right;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function npmOutput(args, opts) {
|
|
47
|
+
const result = spawnSync(npmCommand(), args, Object.assign({
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
shell: false,
|
|
50
|
+
windowsHide: false,
|
|
51
|
+
}, opts || {}));
|
|
52
|
+
if (result.error || result.status !== 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return (result.stdout || '').trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function packageJsonHasDependency(file) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
61
|
+
const sections = [
|
|
62
|
+
parsed.dependencies,
|
|
63
|
+
parsed.devDependencies,
|
|
64
|
+
parsed.optionalDependencies,
|
|
65
|
+
parsed.peerDependencies,
|
|
66
|
+
];
|
|
67
|
+
return sections.some(section => section && section[pkg.name]);
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function findLocalProjectRoot(startDir) {
|
|
74
|
+
let dir = path.resolve(startDir);
|
|
75
|
+
while (true) {
|
|
76
|
+
const packageJson = path.join(dir, 'package.json');
|
|
77
|
+
if (fs.existsSync(packageJson) && packageJsonHasDependency(packageJson)) {
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
const parent = path.dirname(dir);
|
|
81
|
+
if (parent === dir) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
dir = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function localProjectFromPackageDir(packageDir) {
|
|
89
|
+
const nodeModulesDir = path.dirname(packageDir);
|
|
90
|
+
if (path.basename(nodeModulesDir) !== 'node_modules') {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const projectRoot = path.dirname(nodeModulesDir);
|
|
94
|
+
const packageJson = path.join(projectRoot, 'package.json');
|
|
95
|
+
if (fs.existsSync(packageJson)) {
|
|
96
|
+
return projectRoot;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectUninstallPlan(args) {
|
|
102
|
+
if (args.includes('--global') || args.includes('-g')) {
|
|
103
|
+
return {
|
|
104
|
+
scope: 'global',
|
|
105
|
+
npmArgs: ['uninstall', '-g', pkg.name],
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
manualCommand: `npm uninstall -g ${pkg.name}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args.includes('--local')) {
|
|
112
|
+
const projectRoot = findLocalProjectRoot(process.cwd()) ||
|
|
113
|
+
localProjectFromPackageDir(path.resolve(__dirname, '..')) ||
|
|
114
|
+
process.cwd();
|
|
115
|
+
return {
|
|
116
|
+
scope: 'local',
|
|
117
|
+
npmArgs: ['uninstall', pkg.name],
|
|
118
|
+
cwd: projectRoot,
|
|
119
|
+
manualCommand: `cd "${projectRoot}" && npm uninstall ${pkg.name}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
124
|
+
const globalRoot = npmOutput(['root', '-g']);
|
|
125
|
+
if (globalRoot && samePath(packageDir, path.join(globalRoot, pkg.name))) {
|
|
126
|
+
return {
|
|
127
|
+
scope: 'global',
|
|
128
|
+
npmArgs: ['uninstall', '-g', pkg.name],
|
|
129
|
+
cwd: process.cwd(),
|
|
130
|
+
manualCommand: `npm uninstall -g ${pkg.name}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const projectRoot = findLocalProjectRoot(process.cwd()) ||
|
|
135
|
+
localProjectFromPackageDir(packageDir);
|
|
136
|
+
if (projectRoot) {
|
|
137
|
+
return {
|
|
138
|
+
scope: 'local',
|
|
139
|
+
npmArgs: ['uninstall', pkg.name],
|
|
140
|
+
cwd: projectRoot,
|
|
141
|
+
manualCommand: `cd "${projectRoot}" && npm uninstall ${pkg.name}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
scope: 'global',
|
|
147
|
+
npmArgs: ['uninstall', '-g', pkg.name],
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
manualCommand: `npm uninstall -g ${pkg.name}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runFullUninstall() {
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('==========================================');
|
|
156
|
+
console.log(' uniner-cli uninstall');
|
|
157
|
+
console.log('==========================================');
|
|
158
|
+
|
|
159
|
+
const cleanupResult = cleanup({ verbose: true });
|
|
160
|
+
|
|
161
|
+
console.log('');
|
|
162
|
+
const plan = detectUninstallPlan(process.argv.slice(3));
|
|
163
|
+
console.log(`[uniner-cli] Removing ${plan.scope} npm package...`);
|
|
164
|
+
if (plan.scope === 'local') {
|
|
165
|
+
console.log(`[uniner-cli] Project: ${plan.cwd}`);
|
|
166
|
+
}
|
|
167
|
+
const result = spawnSync(npmCommand(), plan.npmArgs, {
|
|
168
|
+
cwd: plan.cwd,
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
shell: false,
|
|
171
|
+
windowsHide: false,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (result.error) {
|
|
175
|
+
console.error('');
|
|
176
|
+
console.error(`[uniner-cli] Failed to run npm uninstall: ${result.error.message}`);
|
|
177
|
+
console.error('[uniner-cli] Runtime cleanup has already been attempted.');
|
|
178
|
+
console.error(`[uniner-cli] Please run manually: ${plan.manualCommand}`);
|
|
179
|
+
console.error('');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const status = result.status === null ? 1 : result.status;
|
|
184
|
+
if (status === 0) {
|
|
185
|
+
console.log('');
|
|
186
|
+
if (cleanupResult.scenariosKept > 0) {
|
|
187
|
+
console.log(`[uniner-cli] Preserved ${cleanupResult.scenariosKept} scenario(s) at:`);
|
|
188
|
+
console.log(`[uniner-cli] ${cleanupResult.scenariosDir}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('[uniner-cli] Uninstall complete.');
|
|
191
|
+
console.log('');
|
|
192
|
+
}
|
|
193
|
+
process.exit(status);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (process.argv[2] === 'uninstall' &&
|
|
197
|
+
(process.argv[3] === '--help' || process.argv[3] === '-h')) {
|
|
198
|
+
printUninstallHelp();
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.argv[2] === 'uninstall') {
|
|
203
|
+
runFullUninstall();
|
|
204
|
+
}
|
|
13
205
|
|
|
14
206
|
const exe = realBinaryPath();
|
|
15
207
|
|
package/lib/SKILL.md.template
CHANGED
|
@@ -5,7 +5,7 @@ description: RPA 自动化 CLI 工具,提供浏览器自动化、Excel 操作
|
|
|
5
5
|
|
|
6
6
|
# uniner-cli Skill
|
|
7
7
|
|
|
8
|
-
> 由 UninerCLI Installer v{{VERSION}} 自动安装到
|
|
8
|
+
> 由 UninerCLI Installer v{{VERSION}} 自动安装到 `~/.claude/skills/uniner-cli/`
|
|
9
9
|
> 安装位置(绝对路径):`{{INSTALL_PATH}}`
|
|
10
10
|
|
|
11
11
|
## 🛑 任务结束硬性规则(最重要,先读这一节)
|
|
@@ -152,7 +152,7 @@ uniner-cli [全局选项] <模块> <子命令> <命令> [命令选项]
|
|
|
152
152
|
## 关键注意事项(6 条 TOC,详情看链接)
|
|
153
153
|
|
|
154
154
|
1. **Chrome CDP 自动管理** — 别手动 kill Chrome;客户机必须装 Chrome 或 Edge
|
|
155
|
-
2. **session 持久化** — `$TEMP
|
|
155
|
+
2. **session 持久化** — `$TEMP/uniner-cli-sessions/sessions.json`(Windows: `%TEMP%\uniner-cli-sessions\sessions.json`);CLI 退出后用 `web browser reconnect` 恢复
|
|
156
156
|
3. **Vue/Element UI 组件** — 日期选择器、级联下拉等,用 `execute-js` 直接操作组件模型,不要硬 input
|
|
157
157
|
4. **desktop 返回 -999** = Agent 服务未启动;此时改用 `web action execute-js` 或 `tool system execute`
|
|
158
158
|
5. **Linux 平台** — Playwright 默认 HTTPS-only,访问 HTTP 站点需改 wrapper 加 `--disable-features=HttpsOnlyMode`,或用 Firefox `--browser-type 1`
|
|
@@ -162,14 +162,14 @@ uniner-cli [全局选项] <模块> <子命令> <命令> [命令选项]
|
|
|
162
162
|
|
|
163
163
|
| 主题 | 路径 |
|
|
164
164
|
|---|---|
|
|
165
|
-
| 命令树全集 + 三级参数 | [{{INSTALL_PATH}}
|
|
166
|
-
| SDK ↔ CLI 命令映射 | [{{INSTALL_PATH}}
|
|
167
|
-
| Windows 配置指南 | [{{INSTALL_PATH}}
|
|
168
|
-
| Linux 配置指南 | [{{INSTALL_PATH}}
|
|
169
|
-
| 实战案例(TMS 抓单) | [{{INSTALL_PATH}}
|
|
170
|
-
| Helper 脚本(Chrome 启动 / Vue 日期 / 表格提取) | [{{INSTALL_PATH}}
|
|
171
|
-
| 用户手册 | [{{INSTALL_PATH}}
|
|
172
|
-
| 工作目录(保存截图/Excel/下载) | [{{INSTALL_PATH}}
|
|
165
|
+
| 命令树全集 + 三级参数 | [{{INSTALL_PATH}}/skills/cli/SKILL.md]({{INSTALL_PATH}}/skills/cli/SKILL.md) |
|
|
166
|
+
| SDK ↔ CLI 命令映射 | [{{INSTALL_PATH}}/skills/cli/sdk-cli-mapping.md]({{INSTALL_PATH}}/skills/cli/sdk-cli-mapping.md) |
|
|
167
|
+
| Windows 配置指南 | [{{INSTALL_PATH}}/skills/windows-setup-guide.md]({{INSTALL_PATH}}/skills/windows-setup-guide.md) |
|
|
168
|
+
| Linux 配置指南 | [{{INSTALL_PATH}}/skills/linux-setup-guide.md]({{INSTALL_PATH}}/skills/linux-setup-guide.md) |
|
|
169
|
+
| 实战案例(TMS 抓单) | [{{INSTALL_PATH}}/skills/tms-workflow-test-report.md]({{INSTALL_PATH}}/skills/tms-workflow-test-report.md) |
|
|
170
|
+
| Helper 脚本(Chrome 启动 / Vue 日期 / 表格提取) | [{{INSTALL_PATH}}/skills/cli/helpers/]({{INSTALL_PATH}}/skills/cli/helpers/) |
|
|
171
|
+
| 用户手册 | [{{INSTALL_PATH}}/docs/]({{INSTALL_PATH}}/docs/) |
|
|
172
|
+
| 工作目录(保存截图/Excel/下载) | [{{INSTALL_PATH}}/workdir/]({{INSTALL_PATH}}/workdir/) |
|
|
173
173
|
|
|
174
174
|
## 典型完整流程(参考)
|
|
175
175
|
|
|
@@ -182,8 +182,8 @@ uniner-cli web browser open --url "https://example.com/login"
|
|
|
182
182
|
# 2. 输账号密码 + 截验证码 + OCR
|
|
183
183
|
uniner-cli web element input --xpath "//input[@id='user']" --value "u01"
|
|
184
184
|
uniner-cli web element input --xpath "//input[@id='pwd']" --value "p01"
|
|
185
|
-
uniner-cli web element screenshot --xpath "//img[@id='captcha']" --output "{{INSTALL_PATH}}
|
|
186
|
-
uniner-cli tool ocr ddddocr --img "{{INSTALL_PATH}}
|
|
185
|
+
uniner-cli web element screenshot --xpath "//img[@id='captcha']" --output "{{INSTALL_PATH}}/workdir/cap.png"
|
|
186
|
+
uniner-cli tool ocr ddddocr --img "{{INSTALL_PATH}}/workdir/cap.png"
|
|
187
187
|
# 从返回 JSON 的 data.result 取识别文本
|
|
188
188
|
|
|
189
189
|
# 3. 提交登录
|
|
@@ -195,8 +195,8 @@ uniner-cli web navigation wait --type element --xpath "//table"
|
|
|
195
195
|
uniner-cli web action execute-js --code "return Array.from(document.querySelectorAll('table tr')).map(r => Array.from(r.cells).map(c => c.textContent.trim()))"
|
|
196
196
|
|
|
197
197
|
# 5. 写 Excel
|
|
198
|
-
uniner-cli excel file create --file "{{INSTALL_PATH}}
|
|
199
|
-
uniner-cli excel data write-range --file "{{INSTALL_PATH}}
|
|
198
|
+
uniner-cli excel file create --file "{{INSTALL_PATH}}/workdir/out.xlsx"
|
|
199
|
+
uniner-cli excel data write-range --file "{{INSTALL_PATH}}/workdir/out.xlsx" --cell A1 --values "<上一步的 JSON 二维数组>"
|
|
200
200
|
|
|
201
201
|
# 6. 关浏览器
|
|
202
202
|
uniner-cli web browser close
|
|
@@ -209,9 +209,11 @@ uniner-cli web browser close
|
|
|
209
209
|
每个场景一个目录,三件套:`meta.json` + `steps.json` + `SKILL.md`,存到:
|
|
210
210
|
|
|
211
211
|
```
|
|
212
|
-
|
|
212
|
+
~/.claude/skills/uniner-cli/scenarios/<kebab-case-name>/
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
(Windows: `%USERPROFILE%\.claude\skills\uniner-cli\scenarios\<kebab-case-name>\`)
|
|
216
|
+
|
|
215
217
|
**4 步标准流程**:
|
|
216
218
|
|
|
217
219
|
```bash
|
|
@@ -331,7 +333,7 @@ Open Claude Code, paste this:
|
|
|
331
333
|
I ran 'uniner-cli skill run' and step 'click-search' failed.
|
|
332
334
|
Error: timeout
|
|
333
335
|
Please look at the scenario at:
|
|
334
|
-
|
|
336
|
+
~/.claude/skills/uniner-cli/scenarios/tms-export
|
|
335
337
|
Especially steps.json. Help me figure out why and suggest a fix.
|
|
336
338
|
──────────────────────────────────
|
|
337
339
|
```
|
|
@@ -368,4 +370,4 @@ uniner-cli skill delete <name> --yes # 删除(默认 dry-run)
|
|
|
368
370
|
- `uniner-cli --help` 看一级
|
|
369
371
|
- `uniner-cli web --help` 看二级
|
|
370
372
|
- `uniner-cli web element click --help` 看具体参数
|
|
371
|
-
- 仍不确定 → Read [{{INSTALL_PATH}}
|
|
373
|
+
- 仍不确定 → Read [{{INSTALL_PATH}}/skills/cli/SKILL.md]({{INSTALL_PATH}}/skills/cli/SKILL.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniner-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "uniner-cli — RPA automation CLI: browser automation (Playwright/CDP), Excel, OCR, desktop/SAP, crypto/HTTP/email tools. Auto-installs a Claude Code skill on install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rpa",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"uniner-cli-uninstall": "bin/uniner-cli-uninstall.js"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"postinstall":
|
|
28
|
+
"postinstall": "node scripts/postinstall.js",
|
|
29
|
+
"preuninstall": "node scripts/preuninstall.js"
|
|
29
30
|
},
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": "\u003e=14"
|
|
@@ -46,4 +47,4 @@
|
|
|
46
47
|
"README.md",
|
|
47
48
|
"LICENSE"
|
|
48
49
|
]
|
|
49
|
-
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Shared cleanup logic for uninstall.
|
|
2
|
+
// Extracted so both the npm lifecycle hook (preuninstall.js) and the
|
|
3
|
+
// manual command (bin/uniner-cli-uninstall.js) can share it.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
rootDir,
|
|
12
|
+
skillDir,
|
|
13
|
+
skillFile,
|
|
14
|
+
} = require('../lib/paths');
|
|
15
|
+
|
|
16
|
+
function rmRf(p) {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(p)) {
|
|
19
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
} catch (_) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rmFile(p) {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(p)) {
|
|
31
|
+
fs.unlinkSync(p);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
} catch (_) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run the full uninstall cleanup.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @param {boolean} [opts.verbose=false] Print progress lines to stdout.
|
|
45
|
+
* The preuninstall hook should pass false
|
|
46
|
+
* because npm swallows lifecycle output
|
|
47
|
+
* unless --verbose is used; the manual
|
|
48
|
+
* command passes true so the user sees
|
|
49
|
+
* what happened.
|
|
50
|
+
* @returns {{ didSomething: boolean, scenariosKept: number, scenariosDir: string|null }}
|
|
51
|
+
*/
|
|
52
|
+
function cleanup(opts) {
|
|
53
|
+
const verbose = (opts && opts.verbose) || false;
|
|
54
|
+
|
|
55
|
+
function log(msg) {
|
|
56
|
+
if (verbose) {
|
|
57
|
+
console.log(`[uniner-cli] ${msg}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const root = rootDir();
|
|
62
|
+
let didSomething = false;
|
|
63
|
+
|
|
64
|
+
// 1. Remove the entire ~/.uniner-cli/ tree (all versions).
|
|
65
|
+
// The version-specific layout exists so the installer can support
|
|
66
|
+
// multi-version coexistence, but on uninstall the user wants a full
|
|
67
|
+
// removal — orphaned old version dirs would just waste disk space.
|
|
68
|
+
if (rmRf(root)) {
|
|
69
|
+
log(`Removed ${root}`);
|
|
70
|
+
didSomething = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir,
|
|
74
|
+
// because the user's accumulated scenarios/<name>/ subdirectories
|
|
75
|
+
// live there and represent real work they've sedimented over time.
|
|
76
|
+
const dir = skillDir();
|
|
77
|
+
if (rmFile(skillFile())) {
|
|
78
|
+
log(`Removed ${skillFile()}`);
|
|
79
|
+
didSomething = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 3. Count scenarios/ subdirs that contain a meta.json.
|
|
83
|
+
let scenariosKept = 0;
|
|
84
|
+
const scenariosPath = path.join(dir, 'scenarios');
|
|
85
|
+
if (fs.existsSync(scenariosPath)) {
|
|
86
|
+
try {
|
|
87
|
+
scenariosKept = fs.readdirSync(scenariosPath).filter(n => {
|
|
88
|
+
const meta = path.join(scenariosPath, n, 'meta.json');
|
|
89
|
+
return fs.existsSync(meta);
|
|
90
|
+
}).length;
|
|
91
|
+
} catch (_) {
|
|
92
|
+
// best-effort
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. If skill dir is now genuinely empty, remove it. Otherwise keep it.
|
|
97
|
+
try {
|
|
98
|
+
if (fs.existsSync(dir)) {
|
|
99
|
+
const remaining = fs.readdirSync(dir);
|
|
100
|
+
if (remaining.length === 0) {
|
|
101
|
+
fs.rmdirSync(dir);
|
|
102
|
+
log(`Removed empty ${dir}`);
|
|
103
|
+
didSomething = true;
|
|
104
|
+
} else if (verbose) {
|
|
105
|
+
log(`Keeping ${dir} (${scenariosKept} scenario(s) preserved)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (_) {
|
|
109
|
+
// best-effort
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!didSomething) {
|
|
113
|
+
log('Nothing to clean — already removed.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
didSomething,
|
|
118
|
+
scenariosKept,
|
|
119
|
+
scenariosDir: scenariosKept > 0 ? scenariosPath : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { cleanup };
|
package/scripts/postinstall.js
CHANGED
|
@@ -28,6 +28,7 @@ const { spawnSync } = require('child_process');
|
|
|
28
28
|
|
|
29
29
|
const {
|
|
30
30
|
pkg,
|
|
31
|
+
rootDir,
|
|
31
32
|
versionDir,
|
|
32
33
|
realBinaryPath,
|
|
33
34
|
isInstalled,
|
|
@@ -237,6 +238,159 @@ function writeSkillMd(installPath, version) {
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
// ----------------------------------------------------------------------
|
|
242
|
+
// Write standalone cleanup script to ~/.uniner-cli/cleanup.js
|
|
243
|
+
//
|
|
244
|
+
// npm 7+ no longer reliably runs `preuninstall` hooks for global packages,
|
|
245
|
+
// so `npm uninstall -g uniner-cli` leaves ~/.uniner-cli/ and the SKILL.md
|
|
246
|
+
// behind. To give users a reliable cleanup path, we write a self-contained
|
|
247
|
+
// cleanup script to ~/.uniner-cli/cleanup.js during install. This file lives
|
|
248
|
+
// OUTSIDE the npm package directory, so it survives `npm uninstall`.
|
|
249
|
+
//
|
|
250
|
+
// Usage after `npm uninstall -g uniner-cli`:
|
|
251
|
+
// node ~/.uniner-cli/cleanup.js
|
|
252
|
+
// ----------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
function writeCleanupScript() {
|
|
255
|
+
const root = path.join(os.homedir(), '.uniner-cli');
|
|
256
|
+
const cleanupPath = path.join(root, 'cleanup.js');
|
|
257
|
+
|
|
258
|
+
// NOTE: this string is written verbatim to disk — no require(), no
|
|
259
|
+
// imports, no template tokens. It must be 100% self-contained Node.js.
|
|
260
|
+
const script = `#!/usr/bin/env node
|
|
261
|
+
// cleanup.js — uninstall cleanup for uniner-cli.
|
|
262
|
+
//
|
|
263
|
+
// WHY THIS FILE EXISTS
|
|
264
|
+
// --------------------
|
|
265
|
+
// npm 7+ (including npm 11) does NOT reliably execute the \`preuninstall\`
|
|
266
|
+
// lifecycle hook for globally installed packages. Running
|
|
267
|
+
// \`npm uninstall -g uniner-cli\` removes the npm package but leaves:
|
|
268
|
+
// - ~/.uniner-cli/ (runtime binaries, skills, docs)
|
|
269
|
+
// - ~/.claude/skills/uniner-cli/SKILL.md (Claude Code skill file)
|
|
270
|
+
//
|
|
271
|
+
// This script was written to this location by the postinstall hook. Because
|
|
272
|
+
// it lives OUTSIDE the npm package directory, it survives \`npm uninstall\`
|
|
273
|
+
// and can always be invoked manually:
|
|
274
|
+
//
|
|
275
|
+
// node ~/.uniner-cli/cleanup.js
|
|
276
|
+
//
|
|
277
|
+
// Idempotent: running it twice is harmless; the second run just reports
|
|
278
|
+
// "nothing to clean".
|
|
279
|
+
//
|
|
280
|
+
// WHAT IT PRESERVES
|
|
281
|
+
// -----------------
|
|
282
|
+
// The \`scenarios/\` subdirectory under
|
|
283
|
+
// ~/.claude/skills/uniner-cli/scenarios/
|
|
284
|
+
// contains user-accumulated knowledge (saved workflows). Those are NEVER
|
|
285
|
+
// deleted by this script — only the single SKILL.md file is removed.
|
|
286
|
+
|
|
287
|
+
'use strict';
|
|
288
|
+
|
|
289
|
+
const fs = require('fs');
|
|
290
|
+
const path = require('path');
|
|
291
|
+
const os = require('os');
|
|
292
|
+
|
|
293
|
+
const rootDir = path.join(os.homedir(), '.uniner-cli');
|
|
294
|
+
const claudeSkillRoot = path.join(os.homedir(), '.claude', 'skills', 'uniner-cli');
|
|
295
|
+
const skillFilePath = path.join(claudeSkillRoot, 'SKILL.md');
|
|
296
|
+
const scenariosDir = path.join(claudeSkillRoot, 'scenarios');
|
|
297
|
+
|
|
298
|
+
function rmRf(p) {
|
|
299
|
+
try {
|
|
300
|
+
if (fs.existsSync(p)) {
|
|
301
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
} catch (_) { return false; }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rmFile(p) {
|
|
309
|
+
try {
|
|
310
|
+
if (fs.existsSync(p)) {
|
|
311
|
+
fs.unlinkSync(p);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
} catch (_) { return false; }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log('==========================================');
|
|
320
|
+
console.log(' uniner-cli uninstall cleanup');
|
|
321
|
+
console.log('==========================================');
|
|
322
|
+
|
|
323
|
+
let didSomething = false;
|
|
324
|
+
|
|
325
|
+
// 1. Remove entire ~/.uniner-cli/ tree (all versions + this script itself).
|
|
326
|
+
if (rmRf(rootDir)) {
|
|
327
|
+
console.log('[uniner-cli] Removed ' + rootDir);
|
|
328
|
+
didSomething = true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 2. Remove ONLY the SKILL.md file. NEVER rm -rf the whole skill dir —
|
|
332
|
+
// scenarios/ lives there and is the user's accumulated knowledge.
|
|
333
|
+
if (rmFile(skillFilePath)) {
|
|
334
|
+
console.log('[uniner-cli] Removed ' + skillFilePath);
|
|
335
|
+
didSomething = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 3. Count preserved scenarios.
|
|
339
|
+
let scenariosKept = 0;
|
|
340
|
+
if (fs.existsSync(scenariosDir)) {
|
|
341
|
+
try {
|
|
342
|
+
scenariosKept = fs.readdirSync(scenariosDir).filter(function(n) {
|
|
343
|
+
return fs.existsSync(path.join(scenariosDir, n, 'meta.json'));
|
|
344
|
+
}).length;
|
|
345
|
+
} catch (_) {}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 4. If the skill dir is now empty, remove it. Otherwise keep it.
|
|
349
|
+
try {
|
|
350
|
+
if (fs.existsSync(claudeSkillRoot)) {
|
|
351
|
+
var remaining = fs.readdirSync(claudeSkillRoot);
|
|
352
|
+
if (remaining.length === 0) {
|
|
353
|
+
fs.rmdirSync(claudeSkillRoot);
|
|
354
|
+
console.log('[uniner-cli] Removed empty ' + claudeSkillRoot);
|
|
355
|
+
didSomething = true;
|
|
356
|
+
} else if (scenariosKept > 0) {
|
|
357
|
+
console.log('[uniner-cli] Keeping ' + claudeSkillRoot +
|
|
358
|
+
' (' + scenariosKept + ' scenario(s) preserved)');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch (_) {}
|
|
362
|
+
|
|
363
|
+
console.log('');
|
|
364
|
+
if (scenariosKept > 0) {
|
|
365
|
+
console.log(' ' + scenariosKept + ' scenario(s) PRESERVED at:');
|
|
366
|
+
console.log(' ' + scenariosDir);
|
|
367
|
+
console.log(' (your accumulated knowledge — not deleted)');
|
|
368
|
+
console.log(' To delete them too:');
|
|
369
|
+
console.log(' rm -rf "' + scenariosDir + '" # bash');
|
|
370
|
+
console.log(' Remove-Item "' + scenariosDir + '" -Recurse -Force # PowerShell');
|
|
371
|
+
console.log('');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (didSomething) {
|
|
375
|
+
console.log('Cleanup done.');
|
|
376
|
+
} else {
|
|
377
|
+
console.log('Nothing to clean — already removed.');
|
|
378
|
+
}
|
|
379
|
+
console.log('');
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
fs.mkdirSync(root, { recursive: true });
|
|
384
|
+
fs.writeFileSync(cleanupPath, script, 'utf8');
|
|
385
|
+
log(`Uninstall cleanup helper written to ${cleanupPath}`);
|
|
386
|
+
log(` After \`npm uninstall -g uniner-cli\`, run:`);
|
|
387
|
+
log(` node ${cleanupPath}`);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// Non-fatal: cleanup.js is a convenience, not required for install to succeed.
|
|
390
|
+
log(`[WARN] Could not write cleanup helper: ${e.message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
240
394
|
// ----------------------------------------------------------------------
|
|
241
395
|
// Main
|
|
242
396
|
// ----------------------------------------------------------------------
|
|
@@ -267,6 +421,9 @@ async function main() {
|
|
|
267
421
|
// Still re-render the skill — it's tiny and lets users edit the template
|
|
268
422
|
// and re-run rebuild to refresh.
|
|
269
423
|
writeSkillMd(versionDir(version), version);
|
|
424
|
+
// Refresh the cleanup helper too — it's idempotent and ensures the
|
|
425
|
+
// script is always present after any postinstall run.
|
|
426
|
+
writeCleanupScript();
|
|
270
427
|
return;
|
|
271
428
|
}
|
|
272
429
|
|
|
@@ -344,6 +501,13 @@ async function main() {
|
|
|
344
501
|
step('Installing Claude Code skill');
|
|
345
502
|
writeSkillMd(targetDir, version);
|
|
346
503
|
|
|
504
|
+
// Write the standalone cleanup helper to ~/.uniner-cli/cleanup.js.
|
|
505
|
+
// This survives `npm uninstall` (the npm package dir is gone but this
|
|
506
|
+
// file persists), giving users a reliable way to clean up residual
|
|
507
|
+
// files even when the `preuninstall` hook doesn't fire.
|
|
508
|
+
step('Writing uninstall cleanup helper');
|
|
509
|
+
writeCleanupScript();
|
|
510
|
+
|
|
347
511
|
console.log('');
|
|
348
512
|
console.log('==========================================');
|
|
349
513
|
console.log(` uniner-cli ${version} installed`);
|
|
@@ -352,6 +516,10 @@ async function main() {
|
|
|
352
516
|
console.log(` Location: ${targetDir}`);
|
|
353
517
|
console.log(` Skill: ${skillFile()}`);
|
|
354
518
|
console.log('');
|
|
519
|
+
console.log(' To uninstall:');
|
|
520
|
+
console.log(' uniner-cli uninstall # auto-detects global vs local install');
|
|
521
|
+
console.log(` node ${path.join(rootDir(), 'cleanup.js')} # fallback cleanup after npm uninstall`);
|
|
522
|
+
console.log('');
|
|
355
523
|
|
|
356
524
|
// macOS 首次运行提示(无代码签名)
|
|
357
525
|
if (platformInfo.platform === 'darwin') {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// preuninstall.js — npm lifecycle hook, runs automatically on
|
|
3
|
+
// `npm uninstall -g uniner-cli`.
|
|
4
|
+
//
|
|
5
|
+
// Cleans up the runtime tree (~/.uniner-cli/<version>/) and the skill file
|
|
6
|
+
// (~/.claude/skills/uniner-cli/SKILL.md). Preserves user scenarios under
|
|
7
|
+
// ~/.claude/skills/uniner-cli/scenarios/ — those represent accumulated
|
|
8
|
+
// knowledge and are never deleted automatically.
|
|
9
|
+
//
|
|
10
|
+
// IMPORTANT: This script MUST exit 0 in all cases, otherwise it would
|
|
11
|
+
// abort the user's `npm uninstall`. Errors are swallowed silently.
|
|
12
|
+
//
|
|
13
|
+
// The manual `uniner-cli-uninstall` command is still useful for users who
|
|
14
|
+
// want verbose feedback, or who want to clean up before the npm package is
|
|
15
|
+
// gone (e.g. if the hook somehow didn't fire).
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { cleanup } = require('./cleanup');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
cleanup({ verbose: false });
|
|
23
|
+
} catch (_) {
|
|
24
|
+
// Never fail the uninstall — swallow all errors.
|
|
25
|
+
}
|